﻿using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Transactions;

namespace DataAccess
{
    /// <summary>
    /// Executes a set of commands under a database transaction
    /// </summary>
    public class DatabaseTransaction
    {
        public DatabaseTransaction()
        {
            Commands = new Queue<DatabaseCommand>();
        }

        #region Properties

        public enum Modes
        {
            Local = 0,
            Distributed
        };

        /// <summary>
        /// Whether the transaction is local cc distributed
        /// </summary>
        internal Modes _Mode { get; set; }

        public enum IsolationLevels
        {
            Unspecified = 0,
            Chaos,
            ReadUncommitted,
            ReadCommitted,
            RepeatableRead,
            Serializable,
            Snapshot,
        }

        /// <summary>
        /// The isolation level of the transaction
        /// </summary>
        internal IsolationLevels _IsolationLevel { get; set; }

        public enum Scopes
        {
            Required = 0,
            RequiresNew,
            Suppress
        }

        /// <summary>
        /// The scope used only for distributed transactions (for local ones is ignored)
        /// </summary>
        internal Scopes _Scope { get; set; }

        /// <summary>
        /// In case of a distributed transaction, this connection is applied to the commands that do not have any connection assigned
        /// In case of a local transaction, this connection is used for the transaction. It must not be null. If any command has an
        /// assigned connections an exception is thrown since local transactions can only use one connection
        /// </summary>
        internal DatabaseConnection _Connection { get; set; }

        /// <summary>
        /// The database commands to be executed under the transaction
        /// </summary>
        public Queue<DatabaseCommand> Commands { get; set; }

        #endregion

        public void Execute()
        {
            if (_Mode == Modes.Distributed)
            {
                ExecuteDistributedTransaction();
            }
            else
            {
                ExecuteLocalTransaction();
            }
        }

        #region Helpers

        /// <summary>
        /// Executes a distributed transaction (different database cc servers)
        /// </summary>
        private void ExecuteDistributedTransaction()
        {
            using (TransactionScope transactionScope = new TransactionScope(GetScope(), new TransactionOptions { IsolationLevel = GetDistributedIsolationLevel() }))
            {
                foreach (DatabaseCommand command in Commands)
                {
                    command.Execute(null);
                }

                transactionScope.Complete();
            }
        }

        private TransactionScopeOption GetScope()
        {
            switch (_Scope)
            {
                case Scopes.Required: return TransactionScopeOption.Required;
                case Scopes.RequiresNew: return TransactionScopeOption.RequiresNew;
                case Scopes.Suppress: return TransactionScopeOption.Suppress;
                default: throw new ArgumentException(string.Format("Unknown transaction scope: {0}", _Scope)); // In case a scope is added
            }
        }

        /// <summary>
        /// Converts to a distributed isolation level
        /// </summary>
        /// <returns></returns>
        private System.Transactions.IsolationLevel GetDistributedIsolationLevel()
        {
            switch (_IsolationLevel)
            {
                case IsolationLevels.Unspecified: return System.Transactions.IsolationLevel.Unspecified;
                case IsolationLevels.Chaos: return System.Transactions.IsolationLevel.Chaos;
                case IsolationLevels.ReadUncommitted: return System.Transactions.IsolationLevel.ReadUncommitted;
                case IsolationLevels.ReadCommitted: return System.Transactions.IsolationLevel.ReadCommitted;
                case IsolationLevels.RepeatableRead: return System.Transactions.IsolationLevel.RepeatableRead;
                case IsolationLevels.Serializable: return System.Transactions.IsolationLevel.Serializable;
                case IsolationLevels.Snapshot: return System.Transactions.IsolationLevel.Snapshot;
                default: throw new ArgumentException(string.Format("Unknown isolation level: {0}", _IsolationLevel)); // In case a new isolation level is added
            }
        }

        /// <summary>
        /// Executes a local transaction (for the same database)
        /// </summary>
        private void ExecuteLocalTransaction()
        {
            DbProviderFactory providerFactory = DbProviderFactories.GetFactory(_Connection.ProviderName);

            using (DbConnection connection = providerFactory.CreateConnection())
            {
                connection.ConnectionString = _Connection.ConnectionString;
                connection.Open();

                DbTransaction transaction = connection.BeginTransaction(GetLocalIsolationLevel());

                try
                {
                    foreach (DatabaseCommand command in Commands)
                    {
                        // Make sure the command has the database driver set
                        if (command._Driver == null)
                        {
                            command._Driver = DatabaseDriverManager.Drivers[_Connection.ProviderName];
                        }

                        command.Execute(new DatabaseContext
                        {
                            Connection = connection,
                            Transaction = transaction
                        });
                    }

                    transaction.Commit();
                }
                catch
                {
                    transaction.Rollback();

                    throw;
                }
            }
        }

        private System.Data.IsolationLevel GetLocalIsolationLevel()
        {
            switch (_IsolationLevel)
            {
                case IsolationLevels.Unspecified: return System.Data.IsolationLevel.Unspecified;
                case IsolationLevels.Chaos: return System.Data.IsolationLevel.Chaos;
                case IsolationLevels.ReadUncommitted: return System.Data.IsolationLevel.ReadUncommitted;
                case IsolationLevels.ReadCommitted: return System.Data.IsolationLevel.ReadCommitted;
                case IsolationLevels.RepeatableRead: return System.Data.IsolationLevel.RepeatableRead;
                case IsolationLevels.Serializable: return System.Data.IsolationLevel.Serializable;
                case IsolationLevels.Snapshot: return System.Data.IsolationLevel.Snapshot;
                default: throw new ArgumentException(string.Format("Unknown isolation level: {0}", _IsolationLevel)); // In case a new isolation level is added
            }
        }

        #endregion

        #region Fluent

        public DatabaseTransaction IsLocal()
        {
            _Mode = Modes.Local;
            return this;
        }

        public DatabaseTransaction IsDistributed(Scopes scope = Scopes.Required)
        {
            _Mode = Modes.Distributed;
            _Scope = scope;
            return this;
        }

        #region Isolation levels

        public DatabaseTransaction Unspecified()
        {
            _IsolationLevel = IsolationLevels.Unspecified;
            return this;
        }

        public DatabaseTransaction Chaos()
        {
            _IsolationLevel = IsolationLevels.Chaos;
            return this;
        }

        public DatabaseTransaction ReadUncommitted()
        {
            _IsolationLevel = IsolationLevels.ReadUncommitted;
            return this;
        }

        public DatabaseTransaction ReadCommitted()
        {
            _IsolationLevel = IsolationLevels.ReadCommitted;
            return this;
        }

        public DatabaseTransaction RepeatableRead()
        {
            _IsolationLevel = IsolationLevels.RepeatableRead;
            return this;
        }

        public DatabaseTransaction Serializable()
        {
            _IsolationLevel = IsolationLevels.Serializable;
            return this;
        }

        public DatabaseTransaction Snapshot()
        {
            _IsolationLevel = IsolationLevels.Snapshot;
            return this;
        }
        
        #endregion

        public DatabaseTransaction Connection(string connectionName)
        {
            _Connection = ConnectionManager.GetConnection(connectionName);
            return this;
        }

        public DatabaseTransaction Command(DatabaseCommand command)
        {
            Commands.Enqueue(command);
            return this;
        }

        #endregion
    }
}
